Описание: Нужно разобраться, как ведут себя пользователи вашего мобильного приложения. Изучим воронку продаж.
Цель исследования
Входные данный
logs_exp.csv - информация о событиях или действиях пользователяОписание данных
EventName — название события;DeviceIDHash — уникальный идентификатор пользователя;EventTimestamp — время события;ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.Ход исследования
Загружаем данные и подготовим их к анализу.
Путь к файлу
/datasets/logs_exp.csv
pandas - библиотека для загрузки и обработки данных
matplotlib.pyplot - библиотека для работы с графиками
datetime - библиотека для работой с датой
numpy - библиотека высокоуровневых математических функций
scipy.stats - библиотека для работы со статистический анализом данных
re - модуль для регулярных выражений
os - библиотека функций для работы с операционной системой
plotly - библиотека визуализации данных (для воронкообразных диаграмм)
import pandas as pd
import numpy as np
import datetime as dt
import scipy.stats as stats
import matplotlib.pyplot as plt
import re
import os
from plotly import graph_objects as go
Функция общей иформации и первичные проверки:
def pre_check(df):
display(df.head(5)) # вывод первых 5 строк
display('Количество пустых ячеек:', df.isna().sum()) # количество пустых ячеек
display('Количество дубликатов:', df.duplicated().sum()) # количество абсолютных дубликатов
df.info() # общая информация
Выведем первые 5 строк таблиц и общую информацию:
pth1 = '/datasets/logs_exp.csv'
pth2 = 'logs_exp.csv'
if os.path.exists(pth1):
logs = pd.read_csv(pth1, sep = '\t')
display('Загрузка онлайн')
elif os.path.exists(pth2):
logs = pd.read_csv(pth2, sep = '\t')
display('Загрузка офлайн')
else:
print('Something is wrong')
pre_check(logs)
'Загрузка офлайн'
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
'Количество пустых ячеек:'
EventName 0 DeviceIDHash 0 EventTimestamp 0 ExpId 0 dtype: int64
'Количество дубликатов:'
413
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
Начнем с названия колонок, напишим функцию и потом применим на нашу таблицу:
def rename_columns(df):
col_list = []
for i in range(len(df.columns)):
col_list.append(re.sub( '(?<!^)(?=[A-Z])', '_', df.columns[i] ).lower().replace('i_d', 'id'))
df.columns=col_list
return df
rename_columns(logs)
display(logs.head(5))
| event_name | device_id_hash | event_timestamp | exp_id | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
Хорошо, перейдем к дубликатам посмотрим на них (возможно были сбои при сборе информации вызвали дублирование строк)
print('Процент дубликатов от общего числа: {0:.2%}'.format(logs.duplicated().sum()/logs.shape[0]))
logs[logs.duplicated()].head(10)
Процент дубликатов от общего числа: 0.17%
| event_name | device_id_hash | event_timestamp | exp_id | |
|---|---|---|---|---|
| 453 | MainScreenAppear | 5613408041324010552 | 1564474784 | 248 |
| 2350 | CartScreenAppear | 1694940645335807244 | 1564609899 | 248 |
| 3573 | MainScreenAppear | 434103746454591587 | 1564628377 | 248 |
| 4076 | MainScreenAppear | 3761373764179762633 | 1564631266 | 247 |
| 4803 | MainScreenAppear | 2835328739789306622 | 1564634641 | 248 |
| 5641 | CartScreenAppear | 4248762472840564256 | 1564637764 | 248 |
| 5875 | PaymentScreenSuccessful | 6427012997733591237 | 1564638452 | 248 |
| 7249 | OffersScreenAppear | 7224691986599895551 | 1564641846 | 246 |
| 8065 | CartScreenAppear | 8189122927585332969 | 1564643929 | 248 |
| 9179 | MainScreenAppear | 2230705996155527339 | 1564646087 | 246 |
Дубликаты появляются при различных событиях и с разными пользователями, нельзя точно сказать из за какой ошибки они возникают. Дубликатов меньше 0.2% так что удаление не сильно отразится на общих данных, можем удалить их:
logs = logs.drop_duplicates()
logs.duplicated().sum()
0
Хорошо дубликатов 0 переходем к дате ивремени.
Преобразуем колонку event_timestamp в формат даты и времени и сделаем одельную колонку с датой:
Преобразуем колонку event_timestamp в формат даты и времени и сделаем одельную колонку с датой:
# преобразуем данные в формат даты и время
logs['event_timestamp'] = logs['event_timestamp'].map(
lambda x: dt.datetime.utcfromtimestamp(x).strftime('%Y-%m-%d %H:%M:%S')
)
logs['event_dt'] = pd.to_datetime(logs['event_timestamp']).dt.date
display(logs.head(5))
| event_name | device_id_hash | event_timestamp | exp_id | event_dt | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
Отлично, переходим дальше.
logs.info()
logs.head(5)
<class 'pandas.core.frame.DataFrame'> Int64Index: 243713 entries, 0 to 244125 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 device_id_hash 243713 non-null int64 2 event_timestamp 243713 non-null object 3 exp_id 243713 non-null int64 4 event_dt 243713 non-null object dtypes: int64(2), object(3) memory usage: 11.2+ MB
| event_name | device_id_hash | event_timestamp | exp_id | event_dt | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
Хорошо мы подготовили таблицу к дальйнешей работе предварительно:
После всех правок у нас осталось таблица с 5 колонками длинной в 243713 значений. Переходим дальше.
Посмотри данные подробнее
Сколько всего событий и как часто они происходят:
print('Всего событий в таблице', logs.shape[0])
round(logs['event_name'].value_counts()/logs.shape[0],2)
Всего событий в таблице 243713
MainScreenAppear 0.49 OffersScreenAppear 0.19 CartScreenAppear 0.18 PaymentScreenSuccessful 0.14 Tutorial 0.00 Name: event_name, dtype: float64
Всего событий в таблице 243 713
MainScreenAppear - Появится главный экран - 49% OffersScreenAppear - Появится экран предложений - 19% CartScreenAppear - Появится экран корзины - 17% PaymentScreenSuccessful - Экран оплаты прошел успешно - 14% Tutorial - Руководство - 0.4%Сколько всего пользователей:
len(logs['device_id_hash'].unique())
7551
Всего 7 551 пользователь.
Сколько событий на одного пользователя:
round(logs.groupby('device_id_hash').agg({'event_name':'count'}).describe())
| event_name | |
|---|---|
| count | 7551.0 |
| mean | 32.0 |
| std | 65.0 |
| min | 1.0 |
| 25% | 9.0 |
| 50% | 20.0 |
| 75% | 37.0 |
| max | 2307.0 |
Посчитаем 95-й и 99-й перцентили количества событий на пользователя и выберем границу для определения аномальных пользователей.
np.percentile(logs.groupby('device_id_hash').agg({'event_name':'count'}), [95, 99])
array([ 89. , 200.5])
200 событий на человека включают 99% пользователей.
Посмотрим как распределены данные:
logs.groupby('device_id_hash').agg({'event_name':'count'}).hist(
bins=100,
figsize=(10,5),
grid=True,
range=(0,200));
plt.title('График количество событий на одного пользователя')# Заголовок графика
# Добавляем подписи к осям:
plt.xlabel('Кол-во событий')
plt.ylabel('Кол-во пользователей');
Убирем аномальных пользователей.
abnormal_users = (
logs.groupby('device_id_hash')
.agg({'event_name':'count'})
.reset_index()
)
# находим пользователей у которых количечтво событий превышает 99% и сохраняем их ID
abnormal_users = (abnormal_users[abnormal_users['event_name']> np.percentile(abnormal_users['event_name'], 99)]
.sort_values('event_name')['device_id_hash'])
# убираем аномальных пользователей из таблицы
logs = logs[~logs['device_id_hash'].isin(abnormal_users)]
print('Количество пользователей:', len(logs['device_id_hash'].unique()))
logs.info()
Количество пользователей: 7475 <class 'pandas.core.frame.DataFrame'> Int64Index: 209333 entries, 0 to 244125 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 209333 non-null object 1 device_id_hash 209333 non-null int64 2 event_timestamp 209333 non-null object 3 exp_id 209333 non-null int64 4 event_dt 209333 non-null object dtypes: int64(2), object(3) memory usage: 9.6+ MB
Остается 209 333 события из 243 713
Посмотрим за какой период у нас дынные:
print('Минимальная дата: {}'.format(logs['event_dt'].min()))
print('Максимальная дата: {}'.format(logs['event_dt'].max()))
print('Продолжительность {} дней'.format((logs['event_dt'].max()-logs['event_dt'].min()).days))
Минимальная дата: 2019-07-25 Максимальная дата: 2019-08-07 Продолжительность 13 дней
Посмотрим на графике как распределены данные:
group_by_date = logs.groupby('event_dt').agg({'device_id_hash':'count'}).reset_index()
fig = plt.figure(figsize=(17, 6)) # Размер графика
# Строим график
plt.plot(group_by_date['event_dt'], group_by_date['device_id_hash'])
plt.title('График распределения данных по дням')# Заголовок графика
# Добавляем подписи к осям:
plt.xlabel('Дата')
plt.ylabel('Кол-во событий');
С учетом графика можно сделать вывод что полными данными мы распологаем только с 1 августа 2019 года по 7 августа 2019 года
Посмотрим на эти данные, сколько их:
cropdate = pd.to_datetime("2019-08-01").date()
len(logs.query("event_dt < @cropdate"))
2718
Какой процент данных от общего числа:
print('Процент неполных данных от общих данных: {0:.2%}'.format(len(logs.query("event_dt < @cropdate"))/logs.shape[0]))
Процент неполных данных от общих данных: 1.30%
Посмотрим сколько пользователей мы отбросим:
print(
'Количество пользователей:',
len(logs['device_id_hash'].unique()) - len(logs.query("event_dt >= @cropdate")['device_id_hash'].unique())
)
Количество пользователей: 17
Посмотрим какой процент пользователей мы отбросим:
print(
'Процент пользователей: {0:.2%}'.format(
1 - len(logs.query("event_dt >= @cropdate")['device_id_hash'].unique()) / len(logs['device_id_hash'].unique())
)
)
Процент пользователей: 0.23%
Всего чуть более 1% данными и меньше 0.3% пользователей, можно ими пренебречь, оставим только полные данные:
logs = logs.query("event_dt >= @cropdate")
logs.head(5)
| event_name | device_id_hash | event_timestamp | exp_id | event_dt | |
|---|---|---|---|---|---|
| 2828 | Tutorial | 3737462046622621720 | 2019-08-01 00:07:28 | 246 | 2019-08-01 |
| 2829 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:00 | 246 | 2019-08-01 |
| 2830 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:55 | 246 | 2019-08-01 |
| 2831 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:08:58 | 246 | 2019-08-01 |
| 2832 | MainScreenAppear | 1433840883824088890 | 2019-08-01 00:08:59 | 247 | 2019-08-01 |
Посмотрим как данные распределены между группами после фильтрации:
mean_group_users = logs.groupby('exp_id').agg({'device_id_hash':'nunique'}).mean()[0] #вычислим среднее количество в группах
group_users = logs.groupby('exp_id').agg({'device_id_hash':'nunique'}).reset_index()
group_users['ratio'] = round((group_users['device_id_hash'] - mean_group_users) / mean_group_users, 2)
group_users.columns = ['exp_id','users','ratio']
print('Всего пользователей:', len(logs['device_id_hash'].unique()))
group_users
Всего пользователей: 7458
| exp_id | users | ratio | |
|---|---|---|---|
| 0 | 246 | 2456 | -0.01 |
| 1 | 247 | 2491 | 0.00 |
| 2 | 248 | 2511 | 0.01 |
Различия в группах от среднего около 1% что допустимо.
Посмотрим как распределены события в отфильтрованных данных:
logs['event_name'].value_counts().reset_index()
| index | event_name | |
|---|---|---|
| 0 | MainScreenAppear | 113264 |
| 1 | OffersScreenAppear | 40956 |
| 2 | CartScreenAppear | 29250 |
| 3 | PaymentScreenSuccessful | 22164 |
| 4 | Tutorial | 981 |
round(logs['event_name'].value_counts()/logs.shape[0],3).reset_index()
| index | event_name | |
|---|---|---|
| 0 | MainScreenAppear | 0.548 |
| 1 | OffersScreenAppear | 0.198 |
| 2 | CartScreenAppear | 0.142 |
| 3 | PaymentScreenSuccessful | 0.107 |
| 4 | Tutorial | 0.005 |
Вот как распределены данные по событиям:
MainScreenAppear - Появится главный экран - 49% OffersScreenAppear - Появится экран предложений - 19% CartScreenAppear - Появится экран корзины - 18% PaymentScreenSuccessful - Экран оплаты прошел успешно - 14% Tutorial - Руководство - 0.4%user_counts = len(logs['device_id_hash'].unique())
print('Всего пользователей:', user_counts)
group_events = (
logs.groupby('event_name')
.agg({'device_id_hash':'nunique'})
.sort_values('device_id_hash', ascending=False)
.reset_index()
)
group_events['ratio'] = round(group_events['device_id_hash'] / user_counts, 3)
group_events.columns = ['event_name','users','ratio']
group_events
Всего пользователей: 7458
| event_name | users | ratio | |
|---|---|---|---|
| 0 | MainScreenAppear | 7344 | 0.985 |
| 1 | OffersScreenAppear | 4517 | 0.606 |
| 2 | CartScreenAppear | 3658 | 0.490 |
| 3 | PaymentScreenSuccessful | 3463 | 0.464 |
| 4 | Tutorial | 824 | 0.110 |
1.5% пользователей минуют главный экран, тут есть несколько предположений.
Предпологаем что воронка строится так
MainScreenAppear > OffersScreenAppear > CartScreenAppear > PaymentScreenSuccessful
"Главный экран" > "Экран продуктов" > "Корзина" > "Успешная оплата"
Обучающие материалы не встраивается в воронку событий, так как не обязательно проходить обучение для совершения покупки.
Посчитаем какой процент пользователей дошел до каждого шага:
funnel = (
logs.groupby('event_name')
.agg({'device_id_hash':'nunique'})
.sort_values('device_id_hash', ascending=False)
.reset_index()
)
funnel.columns = ['event_name','users']
funnel = funnel.query('event_name != "Tutorial"')# убираем из воронки событие "обучение"
funnel['ratio 100%'] = round(funnel['users']/funnel['users'][0] * 100)
funnel['ratio %'] = (round(funnel['users'].pct_change(),2) + 1) * 100
funnel
| event_name | users | ratio 100% | ratio % | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 7344 | 100.0 | NaN |
| 1 | OffersScreenAppear | 4517 | 62.0 | 62.0 |
| 2 | CartScreenAppear | 3658 | 50.0 | 81.0 |
| 3 | PaymentScreenSuccessful | 3463 | 47.0 | 95.0 |
Больше всего людей не уходят дальше главного экрана.
Для большей наглядности построим воронкообразную диаграмму:
fig = go.Figure(go.Funnel(
y = funnel['event_name'],
x = funnel['users'],
textposition = "inside",
textinfo = "value+percent initial")
)
fig.update_layout(dict(title='Интерактивная воронкообразная диаграмма, количество пользователей по событиям',
xaxis= dict(title= 'Количество пользователей'),
yaxis= dict(title= 'Название события')
))
fig.show()
Посмотрим как это отражено в каждой из групп.
Посмотрим сколько осталось пользователей в каждой из групп:
print('Всего пользователей:', len(logs['device_id_hash'].unique()))
logs.groupby('exp_id').agg({'device_id_hash':'nunique'}).reset_index()
Всего пользователей: 7458
| exp_id | device_id_hash | |
|---|---|---|
| 0 | 246 | 2456 |
| 1 | 247 | 2491 |
| 2 | 248 | 2511 |
Посмотрим есть ли пересечения пользователей между группами(попадали ли пользователи в разные группы при проведение теста):
def comparison(gr_a, gr_b, col_gr, col_id, df):
group_a = df[df[col_gr]==gr_a][col_id]
group_b = df[df[col_gr]==gr_b][col_id]
duplicates_users = list(set (group_a) & set ( group_b))
return duplicates_users
print('Количество пользователей попавших в группы 246 и 247:', len(comparison('246','247','exp_id','device_id_hash',logs)))
print('Количество пользователей попавших в группы 246 и 248:', len(comparison('246','248','exp_id','device_id_hash',logs)))
print('Количество пользователей попавших в группы 247 и 248:', len(comparison('247','248','exp_id','device_id_hash',logs)))
Количество пользователей попавших в группы 246 и 247: 0 Количество пользователей попавших в группы 246 и 248: 0 Количество пользователей попавших в группы 247 и 248: 0
Отлично пересечений пользователей нет.
Проверим как прошло контрольное тестирование по группам 246 и 247.
Напишем функцию, для статистической проверки гипотез воспользуемся методом z-тест для пропорций :
def equality_check(t1, t2, s1, s2, alpha):
successes = np.array([s1, s2])
trials = np.array([t1, t2])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / (p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1])) ** 0.5
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = stats.norm(0, 1)
p_value = round((1 - distr.cdf(abs(z_value))) * 2,3)
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
Подготовим данные по группам
groups_sizes = logs.groupby('exp_id').agg({'device_id_hash':'nunique'}).T.reset_index()
groups_sizes.columns.name=''
groups_sizes.rename(columns = {'index':'event_name'}, inplace = True)
groups_sizes['event_name']= 'All'
groups_sizes
| event_name | 246 | 247 | 248 | |
|---|---|---|---|---|
| 0 | All | 2456 | 2491 | 2511 |
Сформулируем гипотезы:
equality_check(
groups_sizes[246][0]+groups_sizes[247][0],
groups_sizes[246][0]+groups_sizes[247][0],
groups_sizes[246][0],
groups_sizes[247][0],
0.05
)
p-значение: 0.482 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Cтатистически значимых отличий между группами нет.
Посмотрим распределение в группах по событиям:
groups_users = (
pd.pivot_table(logs, index='event_name',columns='exp_id', values='device_id_hash', aggfunc='nunique')
.sort_values(246,ascending=False)
.reset_index()
)
groups_users.columns.name=''
groups_sizes = groups_sizes.append(groups_users, ignore_index=True)
groups_sizes = groups_sizes.query('event_name != "Tutorial"')# убираем из воронки событие "обучение"
groups_sizes_ratio = groups_sizes.copy()
groups_sizes_ratio['246_247'] = round(groups_sizes_ratio[246] / (groups_sizes_ratio[246] + groups_sizes_ratio[247]), 2)
groups_sizes_ratio['246_248'] = round(groups_sizes_ratio[246] / (groups_sizes_ratio[246] + groups_sizes_ratio[248]), 2)
groups_sizes_ratio['247_248'] = round(groups_sizes_ratio[247] / (groups_sizes_ratio[247] + groups_sizes_ratio[248]), 2)
groups_sizes_ratio
| event_name | 246 | 247 | 248 | 246_247 | 246_248 | 247_248 | |
|---|---|---|---|---|---|---|---|
| 0 | All | 2456 | 2491 | 2511 | 0.50 | 0.49 | 0.5 |
| 1 | MainScreenAppear | 2423 | 2454 | 2467 | 0.50 | 0.50 | 0.5 |
| 2 | OffersScreenAppear | 1514 | 1498 | 1505 | 0.50 | 0.50 | 0.5 |
| 3 | CartScreenAppear | 1238 | 1216 | 1204 | 0.50 | 0.51 | 0.5 |
| 4 | PaymentScreenSuccessful | 1172 | 1136 | 1155 | 0.51 | 0.50 | 0.5 |
Самое популярное событие MainScreenAppear(Показ главного экрана) - соотношение групп 246 и 247 в этом событии 50/50%
Количество пользователей в различных группах различается не более, чем на 1% что допустимо.
Напишем функцию чтобы сравнить события разных групп:
groups_sizes['246&247'] = groups_sizes[246] + groups_sizes[247]
groups_sizes
| event_name | 246 | 247 | 248 | 246&247 | |
|---|---|---|---|---|---|
| 0 | All | 2456 | 2491 | 2511 | 4947 |
| 1 | MainScreenAppear | 2423 | 2454 | 2467 | 4877 |
| 2 | OffersScreenAppear | 1514 | 1498 | 1505 | 3012 |
| 3 | CartScreenAppear | 1238 | 1216 | 1204 | 2454 |
| 4 | PaymentScreenSuccessful | 1172 | 1136 | 1155 | 2308 |
def cicle_event(df, group1, group2, alpha):
for i in range(1, len(df[group1])):
print(group1,'&', group2, df['event_name'][i])
equality_check(
df[group1][0],
df[group2][0],
df[group1][i],
df[group2][i],
alpha)
print()
alpha = 0.05
Для проверки используем нашу функцию z-тест для пропорций, сформулируем гипотезы:
cicle_event(groups_sizes, 246, 247, alpha)
246 & 247 MainScreenAppear p-значение: 0.673 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 OffersScreenAppear p-значение: 0.277 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 CartScreenAppear p-значение: 0.263 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 PaymentScreenSuccessful p-значение: 0.136 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Cтатистически значимых отличий между группами нет, по всем событиям.
Можно сказать разбитие по группам работает корректно.
Пререйдем к сравнению 246 и 248 групп Сформулируем гипотезы:
cicle_event(groups_sizes, 246, 248, alpha)
246 & 248 MainScreenAppear p-значение: 0.244 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 OffersScreenAppear p-значение: 0.218 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 CartScreenAppear p-значение: 0.083 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 PaymentScreenSuccessful p-значение: 0.224 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Тут так же нет онований отвергнуть нулевую гипотезу что различий нет.
Пререйдем к сравнению 247 и 248 групп Сформулируем гипотезы:
cicle_event(groups_sizes, 247, 248, alpha)
247 & 248 MainScreenAppear p-значение: 0.455 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 OffersScreenAppear p-значение: 0.885 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 CartScreenAppear p-значение: 0.54 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 PaymentScreenSuccessful p-значение: 0.78 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
И здесь нет статистичиски значемых различий.
Пререйдем к сравнению объединенную 246 с 247 группой и 248 группой
Сформулируем гипотезы:
cicle_event(groups_sizes, '246&247', 248, alpha)
246&247 & 248 MainScreenAppear p-значение: 0.262 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 OffersScreenAppear p-значение: 0.428 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 CartScreenAppear p-значение: 0.176 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 PaymentScreenSuccessful p-значение: 0.591 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
С объединенной группой тоже нет оснований считать доли разными.
С учетом что мы сделали 12 проверок, а при уровне 0.1 каждый 10 можно получать ложный результат.
Чтобы снизить групповую вероятность ошибки первого рода и скорректировать требуемые уровни значимости установим уровень равный количеству тестируемых групп применим метод Холма и Шидака
holm_shidak_alpha = round(1 - (1 - 0.05)**(1/12),3)
holm_shidak_alpha
0.004
Зная это уменьшим уровень до 0.004 и перепроверим:
Сформулируем гипотезы:
cicle_event(groups_sizes, 246, 247, holm_shidak_alpha)
cicle_event(groups_sizes, 246, 248, holm_shidak_alpha)
cicle_event(groups_sizes, 247, 248, holm_shidak_alpha)
cicle_event(groups_sizes, '246&247', 248, holm_shidak_alpha)
246 & 247 MainScreenAppear p-значение: 0.673 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 OffersScreenAppear p-значение: 0.277 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 CartScreenAppear p-значение: 0.263 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 247 PaymentScreenSuccessful p-значение: 0.136 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 MainScreenAppear p-значение: 0.244 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 OffersScreenAppear p-значение: 0.218 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 CartScreenAppear p-значение: 0.083 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246 & 248 PaymentScreenSuccessful p-значение: 0.224 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 MainScreenAppear p-значение: 0.455 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 OffersScreenAppear p-значение: 0.885 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 CartScreenAppear p-значение: 0.54 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 247 & 248 PaymentScreenSuccessful p-значение: 0.78 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 MainScreenAppear p-значение: 0.262 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 OffersScreenAppear p-значение: 0.428 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 CartScreenAppear p-значение: 0.176 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 246&247 & 248 PaymentScreenSuccessful p-значение: 0.591 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Так же при измененном параметре различий в количестве пользователей между группами по событиям нет.
Проведя анализ можно сделать вывод что тестирование прошло правильно:
По результатам теста можно остановить тест, признать его успешным пользователей не напугал новый шрифт в приложении.